Skip to content

feat: Avatar and AvatarNameLabel EDS 2.0 - #5089

Open
millus wants to merge 13 commits into
mainfrom
feat/avatar-next
Open

feat: Avatar and AvatarNameLabel EDS 2.0#5089
millus wants to merge 13 commits into
mainfrom
feat/avatar-next

Conversation

@millus

@millus millus commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Resolves #4948

Summary

Adds two new EDS 2.0 components to the /next export: Avatar and AvatarNameLabel.

Avatar

A circular badge displaying a user's initials or profile photo.

Props

  • name — full name of the person. Auto-derives initials (first + last word → e.g. "Ada Lovelace""AL") and sets role="img" + aria-label automatically for screen readers
  • initial — explicit override for the displayed initial(s) (1–2 chars recommended)
  • src — profile photo URL; renders a circular image with object-fit: cover instead of initials
  • alt — alt text for the photo; falls back to name
  • sizesm (16px) | md (24px) | lg (32px)
  • emphasislow (muted accent bg) | high (emphasis bg, white text)
  • notification — success-tone dot indicator at bottom-right; merged into aria-label when name is set (e.g. "Ada Lovelace, notification")

AvatarNameLabel

Composes Avatar with a name and optional metadata label. Accepts all Avatar props plus:

  • name — displayed as the primary label; also auto-derives the avatar initial
  • meta — secondary label (email, job title, or any short string)
  • layouthorizontal (name + meta stacked, for lists) | vertical (name + meta inline, for headers/nav)
  • src / alt — photo support, passed through to the inner Avatar
  • children — open trailing slot for contextual content (overflow menu, role badge, timestamp, etc.)

Accessibility

  • Standalone Avatar: pass name to make it announced to screen readers — no extra attributes needed
  • Avatar inside AvatarNameLabel: decorative (the visible name text provides context)
  • Notification dot is merged into the avatar label ("Ada Lovelace, notification") for standalone — inside AvatarNameLabel a visually-hidden span announces it alongside the name
  • Long names wrap naturally; no overflow:hidden truncation (avoids descender clipping with text-box tokens)

Figma Code Connect

Connects both Avatar (node 9319-5410) and AvatarNameLabel (node 9319-5429) including the Photo variant.

Deviations from Figma

--eds-selectable-space-* gap tokens in AvatarNameLabel — Figma pairs --eds-selectable-space-horizontal with horizontal layout and --eds-selectable-space-vertical with vertical layout. Our code uses them by gap axis direction (consistent with how Input uses padding-block/padding-inline). Both tokens are the same value so there is no visual difference — needs a decision on intended semantics.

Test plan

  • 39 unit tests passing (Avatar.test.tsx)
  • axe accessibility tests pass for all variants
  • Check Storybook — Avatar and AvatarNameLabel stories render correctly
  • Verify controls panel categories (Core / Appearance / States)
  • Verify photo variant renders correctly at all three sizes
  • Verify notification announced correctly in both standalone and AvatarNameLabel contexts

🤖 Generated with Claude Code

@millus

millus commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

@claude please review

@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown

Claude finished @millus's task in 2m 36s —— View job


Code review: Avatar & AvatarNameLabel

  • Read changed files
  • Reviewed against EDS conventions (structure, CSS, tokens, a11y, tests)
  • Posted review feedback

Nicely structured PR — file layout, forwardRef + displayName, named-only exports, @layer eds-components, --_ pseudo-private vars, data-* variants and the dynamic data-color-appearance pattern all match the /next conventions. The notification dot's data-color-appearance="success" + --eds-color-bg-fill-emphasis-default remap is the correct dynamic-token approach, and all the tokens referenced resolve. A few things worth addressing before merge:


🔴 Accessibility — the initial has no meaning to a screen reader

This is the main gap. A standalone <Avatar initial="C" /> renders a <div> whose only content is the text node C (Avatar.tsx:26). A screen reader announces a bare "C", which is meaningless. The jest-axe tests pass only because they manually add aria-label — the component itself neither hides the initial nor surfaces a label prop, so the default usage is inaccessible.

Worse, inside AvatarNameLabel the initial duplicates the adjacent visible name: <Avatar initial="A"> sits right before Ada Lovelace, so the row is announced as "A Ada Lovelace ada@example.com". The initial there is purely decorative and should be hidden.

Recommendation:

  • Mark the .initial span aria-hidden="true" (the letter is decorative; the meaningful identity comes from a label or the adjacent name).
  • For the standalone case, document that consumers should pass aria-label (already spread via ...rest), or follow the role="img" + label pattern used elsewhere in /next.

This also makes the axe tests pass for the real default render, not just the labelled one. Fix this →


🟠 Horizontal / vertical spacing tokens look swapped — avatar.css

The gap tokens don't match the axis they're applied to:

  • avatar.css:73.eds-avatar-name-label is a row but uses gap: var(--eds-container-gap-vertical). A horizontal gap should use --eds-container-gap-horizontal.
  • avatar.css:89.names is a column (stacked name/email) but uses gap: var(--eds-selectable-space-horizontal) for its vertical gap.

.content at line 79 (a row using selectable-space-horizontal) is the one that's correct. Please double-check each gap against its flex direction.


🟠 Hardcoded pixel values instead of tokens — avatar.css

The convention is to use --eds-* tokens and never hardcode dimensions:

If there's no dedicated dot-size token, consider deriving from a sizing token or at least flag these as intentional. The 8/10/12px magic numbers won't track density changes.


🟡 initial = 'A' is a misleading default — Avatar.tsx:8

Defaulting initial to 'A' means <Avatar /> silently shows a real-looking letter that doesn't correspond to anyone. An empty default (or requiring the prop) would be safer; AvatarNameLabel already derives the initial from fullName, so the 'A' fallback there (AvatarNameLabel.tsx:21) has the same issue.


🟡 layout values are counter-intuitive — AvatarNameLabel.types.ts:11

layout="horizontal" stacks name/email vertically (column), and layout="vertical" puts them inline (row). That's the opposite of what the names suggest and is a public-API footgun. If this mirrors Figma naming, fine — but please make the JSDoc explicit, or consider renaming (e.g. stacked / inline).


🟡 Minor

  • <p> for name/email (AvatarNameLabel.tsx:36,38) — these aren't paragraphs; <span> (with the flex layout) is more semantically honest.
  • Notification background-color is set directly (avatar.css:67) rather than through a --_ var — the convention prefers overriding a variable, though it's constant here so low priority.
  • Test gapAvatarNameLabel forwards size / emphasis / notification to the inner Avatar, but none of those pass-throughs are tested (only slot/layout/initial are). The Avatar.figma.tsx AvatarNameLabel connect also doesn't map the nested avatar props (figma.nestedProps per AGENTS) — fine if not in the design.
  • The two import … from '.' lines in the stories/figma files could be combined.

Overall this is close. The accessibility handling of the initial is the one I'd consider blocking; the token-axis mismatches and hardcoded px are quick fixes.
· feat/avatar-next

@millus millus changed the title feat: Avatar and AvatarNameLabel EDS 2.0 components feat: Avatar and AvatarNameLabel EDS 2.0 Jun 22, 2026
@millus

millus commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

@claude please review

@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown

Claude finished @millus's task in 1m 22s —— View job


Code review: Avatar & AvatarNameLabel (re-review)

  • Read all changed files
  • Checked current state against the previous review
  • Reviewed against EDS conventions (structure, CSS, tokens, a11y, tests)

Good progress since the last pass — commit de128c7 resolved the blocking accessibility issue and most of the medium items. Summary below.


✅ Resolved since last review

  • Initial is now aria-hidden (Avatar.tsx:30) and the role="img" + aria-label pattern is documented on the initial prop (Avatar.types.ts:11-15). The duplicate-announcement problem inside AvatarNameLabel is gone — the inner avatar is now purely decorative. 👍
  • Gap tokens fixed — row container uses --eds-container-gap-horizontal (avatar.css:80), stacked .names column uses --eds-selectable-space-vertical (line 98), and the inline (vertical) variant switches to --eds-selectable-space-horizontal (line 122). Each gap now matches its axis.
  • initial no longer defaults to 'A' in Avatar — empty render shows no span.
  • <span> instead of <p> for name/email.
  • Notification colour goes through --_notification-bg (avatar.css:14,72) and the dynamic data-color-appearance="success" remap is correct.
  • layout JSDoc now explains the counter-intuitive naming and ties it to the Figma convention.
  • Pass-through tests added — there's now an Avatar pass-throughs block, and the unlabelled-default axe test exists.
  • Imports combined in stories/figma.

🟡 Remaining (all minor — non-blocking)

  1. 'A' fallback still lingers in AvatarNameLabel (AvatarNameLabel.tsx:21): initial ?? (fullName?.[0]?.toUpperCase() || 'A'). Since fullName is a required string (AvatarNameLabel.types.ts:8), both the optional chaining and the 'A' fallback are dead paths for typed callers — they only trigger on an empty string fullName="", in which case a stray 'A' is arguably more confusing than rendering nothing. Consider initial ?? fullName.charAt(0).toUpperCase() (yields '' for empty input, which the {initial && …} guard already handles). Fix this →

  2. size / emphasis pass-throughs still untested. The new block covers notification only; size and emphasis flow through to the inner Avatar (AvatarNameLabel.tsx:30-31) with no assertion. A quick data-size / data-emphasis check on the inner avatar would close the gap.

  3. Long name/email can overflow. .names sets min-width: 0 (avatar.css:102) which enables shrinking, but .full-name / .email have no overflow: hidden; text-overflow: ellipsis; white-space: nowrap. A long email in the horizontal layout will overflow the container rather than truncate. Worth deciding whether truncation is intended.

  4. Hardcoded dot sizes / offsets remain (--_dot-size: 12px/8px/10px, right/bottom: -2px). These are now clearly commented as intentional-per-Figma with no token equivalent, which is acceptable — flagging only so it's a conscious decision. They won't track density changes.

  5. AvatarNameLabel Code Connect doesn't map the nested avatar props (Avatar.figma.tsx:36-46) — no figma.nestedProps for size/emphasis/notification. Fine if those aren't part of the Figma AvatarNameLabel variant set; otherwise the generated snippet under-represents the API.


Overall this is in good shape — the previously-blocking a11y issue is fixed and the token/axis corrections are right. The remaining items are all polish and can land in this PR or a follow-up. Nice work. ✅
· feat/avatar-next

@millus millus self-assigned this Jun 24, 2026
millus added 7 commits June 24, 2026 12:01
Initial implementation of Avatar (initial-based, three sizes, two emphasis
levels, optional notification dot) and AvatarNameLabel (horizontal and
vertical layouts) based on Figma designs.
- Add `name` prop: auto-derives initials and sets role="img" + aria-label
- Merge notification into avatar aria-label ("Ada Lovelace, notification")
  to fix nested role="img" violation
- Make Avatar inside AvatarNameLabel decorative — fullName text provides context
- Rename `email` → `meta` for generic secondary label support
- Switch AvatarNameLabel to display:flex; truncate long names, wrap meta
- Derive two-letter initials from first+last word of fullName
- Remove data-testid from component internals; use role/text queries in tests
- Add slot story with overflow menu, role badge, and timestamp examples
- Add edge cases story showing long name wrapping and multi-part initials
- Add argTypes with categories to Avatar and AvatarNameLabel stories
- Switch AvatarNameLabel to display:flex; long names wrap naturally
- Remove overflow:hidden/ellipsis from full-name — no descender clipping
- Remove unused --_notification-bg variable and duplicate font-family
- Consolidate max-width into shared selector, use align-items:stretch
- Restore text-box:trim-both cap alphabetic with correct scoping
- Add src and alt props — renders circular profile photo with object-fit:cover
- When src is provided, img element carries the accessible name via alt
- Initials are not shown when a photo is present
- Notification dot still renders on photo avatars
- Add Photo story and two tests (renders img, hides initials)
- Update Figma Code Connect to handle Photo variant
…o NameLabel

- Rename AvatarNameLabel fullName prop to name for API consistency with Avatar
- Add src and alt props to AvatarNameLabel — passes through to inner Avatar
- Update Figma Code Connect to map figma.string('Full Name') to name prop
- Update all tests, stories, types, and descriptions to match
- Add NameLabelWithPhoto story and update slot story with real photos
- Add src/alt to argTypes under Core category
- Reorder stories: Notification before Photo on both Avatar and NameLabel
@millus
millus force-pushed the feat/avatar-next branch from 30baa9d to 0674115 Compare June 24, 2026 10:02
- Add size and emphasis pass-through tests for AvatarNameLabel
- Add size, emphasis, notification to AvatarNameLabel Figma Code Connect
@millus
millus marked this pull request as ready for review June 24, 2026 10:07
@millus

millus commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

@claude please review

@claude

claude Bot commented Jun 24, 2026

Copy link
Copy Markdown

Claude finished @millus's task in 1m 41s —— View job


Code review: Avatar & AvatarNameLabel (re-review)

  • Read changed files
  • Reviewed against EDS conventions (structure, CSS, tokens, a11y, tests)
  • Posted review feedback

Since the last pass the PR added photo/image support and renamed fullNamename. The structural conventions still look good — file layout, forwardRef + displayName, named-only exports, correct registration in index.ts / index.css, @layer eds-components, --_ pseudo-private vars, data-* variants, and the dynamic data-color-appearance pattern. The earlier blocking a11y issue (decorative initial) and the token-axis fixes are still in place. A few items — most introduced by the new photo support — are worth a look.


🟠 Photo inside AvatarNameLabel duplicates the name for screen readers

AvatarNameLabel renders the inner Avatar without the name prop (correct — keeps the initials avatar decorative), but for the photo path it passes alt={alt ?? name} (AvatarNameLabel.tsx:35). That gives the <img> an accessible name equal to the adjacent visible .full-name, so a screen reader announces the person twice:

"Ada Lovelace, image — Ada Lovelace — ada@example.com"

This is the same duplicate-announcement problem the first review flagged for the initial, now reintroduced through the photo. Inside the composed component the avatar image should be decorative by default — alt={alt ?? ''} — so the visible text is the single source of the name (consumers can still pass an explicit alt if they really want one). Worth an axe/role assertion for <AvatarNameLabel src=… /> too, since no test currently covers the photo path inside the label. Fix this →


🟡 deriveInitials('') throws

name is a required string on AvatarNameLabel, but the type allows "", and AvatarNameLabel calls deriveInitials(name) unconditionally when no initial override is given (AvatarNameLabel.tsx:23). For name="" (or whitespace-only), deriveInitials does ''.split(/\s+/)[''], then words[0][0] is undefined, and undefined.toUpperCase() throws a TypeError (Avatar.tsx:6). Standalone Avatar is safe (it guards with name ? … : undefined), so the crash is AvatarNameLabel-only. A small guard in deriveInitials (if (!words[0]) return '') closes it.


🟡 size / emphasis pass-through tests don't actually assert the pass-through

The new Avatar pass-throughs block (Avatar.test.tsx:193-208) only asserts that the name text renders — it never checks that the inner Avatar received data-size / data-emphasis. These would pass even if the props were dropped on the floor. Querying the inner avatar and asserting data-size="sm" / data-emphasis="high" would close the gap that the previous review raised.


🟡 Stale notification JSDoc

Avatar.types.ts:31 says the notification is "Announced to screen readers as 'Notification' via role="img" and aria-label". That's not what the code does: the dot span is aria-hidden, and the word is only merged into the avatar's aria-label when name is set (${name}, notification). With no name — and inside AvatarNameLabel, where name isn't forwarded to the inner avatar — the notification is not announced at all. Worth correcting the doc to match the actual behaviour (and deciding whether the notification state should be surfaced to SR users in the label case).


🟡 Minor

  • Code Connect — the photo branch of the Avatar connect (Avatar.figma.tsx:23-28) drops emphasis/initial (fine, photo replaces initials) but the AvatarNameLabel connect has no src/Photo mapping, so the generated snippet under-represents the new photo variant if the Figma AvatarNameLabel has one.
  • Long-name overflow.meta now has overflow-wrap: break-word; .full-name doesn't. Per the PR description wrapping (no truncation) is intentional, so this is just a consistency note.

Overall this is in good shape and close to merge. The photo-in-AvatarNameLabel duplicate announcement is the one I'd address before merging (a one-line alt default); the rest are polish.
· feat/avatar-next

millus added 2 commits June 24, 2026 13:40
…CSS, Figma

- Photo inside AvatarNameLabel is now decorative (alt="" by default) — avoids
  duplicate name announcement next to visible fullName text
- deriveInitials guards against empty/whitespace name to prevent TypeError
- Add axe test for AvatarNameLabel with photo
- Fix notification JSDoc to accurately describe when/how it is announced
- Add overflow-wrap:break-word to .full-name for consistency with .meta
- Add Photo/src variant to AvatarNameLabel Figma Code Connect
- Document why size/emphasis pass-through tests are smoke tests
@millus

millus commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

@claude please re-review, is this good to go now? no blockers, ready for human review?

@claude

claude Bot commented Jun 24, 2026

Copy link
Copy Markdown

Claude finished @millus's task in 1m 13s —— View job


Re-review: Avatar & AvatarNameLabel — go/no-go

  • Read current state of changed files
  • Verified previously-flagged items are addressed
  • Checked CI status
  • Posted assessment

Verdict: ✅ No blockers — ready for human review.

Every item flagged in the last review is resolved, and all 5 CI checks are green (Checks, Code Quality, PR #5089, React 18 compatibility, PR Title Check).

✅ Resolved since last pass (commit 74a0661)

  • Photo no longer duplicates the name in AvatarNameLabel — inner avatar now gets alt={alt ?? ''} (AvatarNameLabel.tsx:35), so it's decorative and the visible text is the single source of the name. Backed by a new axe test for the photo-in-label path (Avatar.test.tsx:256). 👍
  • deriveInitials('') no longer throws — guarded with if (!words[0]) return '' (Avatar.tsx:6).
  • Stale notification JSDoc corrected — now accurately states the dot is aria-hidden and only merged into the avatar aria-label when name is set (Avatar.types.ts:31).
  • Code Connect now maps the Photo variant for AvatarNameLabel via isPhotosrc (Avatar.figma.tsx:61,79).
  • Earlier fixes still in place: decorative initial (aria-hidden), correct gap-axis tokens, --_ notification colour with data-color-appearance="success", hardcoded dot sizes/offsets clearly commented as intentional-per-Figma.

🟡 Two tiny nits (non-blocking — fine to land as-is or tidy later)

  1. alt JSDoc on AvatarNameLabel is now slightly stale. AvatarNameLabel.types.ts:27 still reads "Falls back to name if not provided", but the component intentionally defaults to '' (decorative) now. Suggest "Alt text for the photo. Defaults to empty (decorative) so the visible name isn't announced twice; pass a value to override." Fix this →

  2. size/emphasis pass-through tests assert render-without-error, not the actual wiring (Avatar.test.tsx:202-210). The inline comment explains this is a deliberate choice to respect Testing Library's no-node-access rule and lean on the type system — a reasonable trade-off. If you want a real assertion without querySelector, within(screen.getByText('Ada').closest(...)) is awkward; simplest is to give the inner avatar a stable test hook. Genuinely optional.

Neither is a blocker. This is in good shape to hand to a human reviewer. Nice work iterating through the rounds. ✅
· feat/avatar-next

…ls, token/JSDoc fixes

- Add visually-hidden .notification-label span in AvatarNameLabel so screen
  readers announce notification status alongside the visible name
- Remove export from deriveInitials in Avatar.tsx — internal detail, now
  duplicated locally in AvatarNameLabel to avoid leaky coupling
- Revert gap token to --eds-container-gap-horizontal (semantically correct
  for a horizontal row arrangement)
- Clarify --_color comment in avatar.css
- Fix stale alt JSDoc in AvatarNameLabel.types.ts
- Add axe + presence tests for notification in AvatarNameLabel

@pomfrida pomfrida left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really solid PR overall. The component split between Avatar and AvatarNameLabel is clean, the accessibility model is well-thought-out across both contexts (the notification merged into aria-label for standalone, visually-hidden span for the composed component — two different solutions for two different contexts, and both right), the pseudo-private --_ variable pattern is applied correctly for size scaling, and the Figma Code Connect coverage for both components (including the Photo variant) is more than the bar. Tests are organised into Rendering / Variants / Notification / Accessibility blocks with a jest-axe assertion per meaningful variant — exactly what AGENTS.md asks for. Thanks also for pre-flagging the --eds-selectable-space-* deviation in the description.

A few inline comments split across three buckets:

  • 🔴 Blockers (please address before merge): deriveInitials duplicated across both files (1), and the size / emphasis pass-through tests in AvatarNameLabel don't actually assert anything (2)
  • 🟡 Worth a look: vertical-layout overflow at narrow viewports with long names (3, verified in Storybook with a concrete CSS fix proposed), generic data-size attribute name (4), and a few smaller a11y / token observations
  • 🟢 Nits / follow-up: stylistic cleanups and one broader observation about hardcoded values in /next stories that isn't specific to this PR

Comment on lines +5 to +10
function deriveInitials(name: string): string {
const words = name.trim().split(/\s+/)
if (!words[0]) return ''
if (words.length === 1) return words[0][0].toUpperCase()
return (words[0][0] + words[words.length - 1][0]).toUpperCase()
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deriveInitials is defined identically in both Avatar.tsx and AvatarNameLabel.tsx. Move it to a single source so the two can't drift. Either Avatar/utils.ts (component-local) or components/next/utils/ (shared) — the rest of /next keeps small helpers component-local, so a utils.ts next to Avatar.tsx and an import from both files would match existing conventions.

Comment on lines +199 to +212
// size and emphasis flow to the inner Avatar via props — verified visually
// and by TypeScript. Testing Library's no-node-access rule prevents querying
// the inner div's data attributes directly; the type system enforces the wiring.
it('renders with size prop without error', () => {
render(<AvatarNameLabel name="Ada" size="sm" />)
expect(screen.getByText('Ada')).toBeInTheDocument()
})

it('renders with emphasis prop without error', () => {
render(<AvatarNameLabel name="Ada" emphasis="high" />)
expect(screen.getByText('Ada')).toBeInTheDocument()
})
})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests only assert that the visible name still renders — they don't verify that size or emphasis actually reach the inner Avatar. The comment says "Testing Library's no-node-access rule prevents querying the inner div", but screen.getByText('AL').closest('.eds-avatar') (or a data-testid on the inner Avatar) would let us assert data-size / data-emphasis directly. As written, the inner Avatar could be rendered with hard-coded defaults and these tests would still pass.

Suggestion:

it('passes size to the inner Avatar', () => {
  render(<AvatarNameLabel name="Ada Lovelace" size="sm" />)
  const avatar = screen.getByText('AL').closest('.eds-avatar')
  expect(avatar).toHaveAttribute('data-size', 'sm')
})

Comment on lines +28 to +32
&[data-size='sm'] {
--_size: var(--eds-sizing-icon-xs); /* 16px */
--_font-size: var(--eds-typography-ui-body-xs-font-size);
--_line-height: var(--eds-typography-ui-body-xs-line-height-default);
--_dot-size: 8px;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I see all other /next components scope their size attribute (data-icon-size, data-selectable-space). data-size is the first plain variant in /next and can collide with ancestor styles or future tokens that key off [data-size]. Suggest renaming to data-avatar-size for consistency. (data-emphasis is fine — Badge already uses it.)

& .names {
flex: initial;
flex-direction: row;
flex-shrink: 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In data-layout="vertical" the .content and .names blocks both have flex-shrink: 0, with white-space: nowrap on .names and no truncation fallback. Verified in Storybook via the NameLabelEdgeCases story (which uses deliberately long names like "Bartholomew Featherstonehaugh"):

  • At a 375px viewport, the .names row is 44px wider than the body — the email gets clipped on the right edge (macOS hides the scrollbar by default, so visually it looks like the text just disappears; Windows / Linux / iOS would show a horizontal scrollbar on <body>)
  • At a 240px viewport, the row is 347px wide inside a 176px container — ~50% of the email content is hidden

Acknowledged that this only triggers with the combination of (a) a narrow container and (b) long names, and that the layout is documented as "intended for wider contexts". But once both conditions hit, the email is invisible with no indicator that more text exists, which is worse than truncating with an ellipsis (where the at least signals "there's more"). Suggest adding a defensive truncation so the component degrades gracefully — note that both .content and .names need to be allowed to shrink, otherwise the truncation doesn't engage:

&[data-layout='vertical'] {
  & .content {
    flex: 1 1 auto;     /* was: flex: initial */
    flex-shrink: 1;     /* was: 0 — must let parent shrink */
    min-width: 0;
  }

  & .names {
    flex: 1 1 auto;     /* was: flex: initial */
    flex-shrink: 1;     /* was: 0 */
    min-width: 0;       /* required for text-overflow inside flex */
    /* flex-direction: row, white-space: nowrap kept */
  }

  & .meta {
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
  }
}

Verified locally — with the patch applied at 375px viewport, body overflow drops from 44px to 0px and the email shows as "b.featherstoneha…". Alternative: drop white-space: nowrap to allow wrapping (preserves information at the cost of multi-line).

--_line-height: var(--eds-typography-ui-body-md-line-height-default);

/* Dot sizes have no token equivalent in the design — hardcoded per Figma spec */
--_dot-size: 12px;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged in the comments as "no token equivalent". Could you eave hard-coded but maybe reference the Figma spec node id in the comment so future readers can verify, in case it would be added later

size={size}
emphasis={emphasis}
src={src}
alt={alt ?? ''}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alt={alt ?? ''} is passed to the inner Avatar, but Avatar itself does alt={alt ?? name ?? ''} (line 36 of Avatar.tsx) — so the alt prop on AvatarNameLabel does work via override, but the doc comment in AvatarNameLabel.types.ts:27 says it defaults to "" so the visible name isn't announced twice. Good, but worth a one-line code comment here explaining why we force '' rather than letting Avatar's own fallback to name run, since the visible name covers the image.

'The `children` prop renders into a trailing slot to the right of the name. It is open — use it for anything contextual to that person, like an action button, a role badge, or a timestamp. The examples here are just starting points.',
},
},
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker for this PR — Avatar.stories.tsx is fully consistent with how the other /next stories (Button, Badge, Icon, Input) handle demo styling. Flagging it as a broader observation rather than a change request: across /next stories we routinely inline raw #hex colours and '12px' / '16px' pixel values for demo scaffolding (section headings, gaps, sample backgrounds). Examples outside this PR:

  • Icon.stories.tsxcolor: '#666', background: '#f5f5f5'
  • Input.stories.tsxbackground: '#f7f7f7', '--eds-color-neutral-1': '#fff'
  • Badge.stories.tsxfontSize: '14px', fontSize: '16px'

Storybook is one of the most-visible surfaces of the design system, so demo styling that bypasses the token system is a bit of a "do as I say, not as I do" signal. Worth a separate conversation/PR about whether we want to tighten this up across the board (an .sb-section-heading utility or a shared story-helper component would cover most of the repeated patterns). So I think I will add a task for conforming it to our tokens for us to tackle after we have landed the new token structure. No action needed in this PR.

{...rest}
>
{src ? (
<img className="photo" src={src} alt={alt ?? name ?? ''} />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSS sizes the image with position: absolute; width: 100%; height: 100%, so layout is stable, but adding explicit width / height attributes (matching the rendered pixel size) helps browsers reserve space before CSS loads and is a small CLS improvement. Optional.

expect(screen.getByText('X')).toBeInTheDocument()
})

it('renders slot right when children provided', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test asserts that the slot content renders, but not that it ends up in .slot-right. A .toHaveClass check on the closest container would catch a regression where someone moved children into .names.

<span className="full-name">{name}</span>
{meta && <span className="meta">{meta}</span>}
{notification && (
<span className="notification-label">Notification</span>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good a11y instinct: when the avatar is decorative inside AvatarNameLabel, the notification can't ride on the avatar's aria-label, so you've added a visually-hidden span instead. Two different solutions for two different contexts, and both are right.

@vnys
vnys removed their request for review August 3, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Avatar] Implement in code

2 participants